﻿namespace Sai.Test
{
    using Sai.Cecil;
    using Sai.Core.Introspection;
    using Sai.Test.TestDataBuilders;
    using Sai.Test.XUnit;
    using Xunit;

	public class IntrospectedAssemblyFacts
	{
		public class TheTypesInMethod
		{
			[Fact, TouchingFileSystemTest]
			public void ReturnsEmptyListForMissingNamespace()
			{
				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account");

				string code = BoilerPlateCodeBuilder.BuildFor(classBuilder);

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

					Assert.Equal(0, result.TypesIn("ApiEx2").Count);
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsListOfOneForSingleClassInNamespace()
			{
				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account")
										.WithProperty("public double CurrentBalance { get; set; } ")
										.WithMethod("public double CalculateCharges() { return CurrentBalance; }")
										.WithField("private double _currentBalance = 0.0f;");

                string code = BoilerPlateCodeBuilder.BuildFor(classBuilder);

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

                    Assert.Equal(1, result.TypesIn("Example").Count);
				}
			}
		}

		public class TheLookupMethod
		{
			[Fact, TouchingFileSystemTest]
			public void ReturnsNullForMissingType()
			{
				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account");

                string code = BoilerPlateCodeBuilder.BuildFor(classBuilder);

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

					Assert.Null(result.Lookup(new TypeName(new FullyQualifiedName("ApiEx.Customer"))));
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsCorrectTypeForCreatedType()
			{
				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account");

                string code = BoilerPlateCodeBuilder.BuildFor(classBuilder);

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

                    Assert.Equal("Example.Account", result.Lookup(new TypeName(new FullyQualifiedName("Example.Account"))).TypeName.FullName.ToString());
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsFullyInitializedObject()
			{
				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account")
										.WithConstructor("public Account() { }")
										.WithConstructor("public Account(int amount) { }")
										.WithField("int _amount = 0;")
										.WithField("public int PublicAmount = 0;")
										.WithMethod("public void Add(int value) { this._amount += value; } ")
										.WithProperty("public int Amount { get { return this._amount; } set { this._amount = value; } }");

                string code = BoilerPlateCodeBuilder.BuildFor(classBuilder);

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

					ITypeDeclaration introspectedInfo = result.Lookup(new TypeName(new FullyQualifiedName("Example.Account")));

					ClassDeclaration refDec = introspectedInfo as ClassDeclaration;

					Assert.NotNull(refDec);

					Assert.Equal(2, refDec.InstanceConstructors.Count);
					Assert.Equal(1, refDec.Methods.Count);
					Assert.Equal(1, refDec.Properties.Count);
					Assert.Equal(1, refDec.Fields.Count); // _amount is private
					Assert.False(refDec.IsObsolete);
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsObjectImplementingSingleInterface()
			{
				var interface1Builder = CompilableInterfaceBuilder.PublicInterface()
										.Named("IEmptyInterface1");

				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account")
										.Implementing("IEmptyInterface1")
										.WithConstructor("public Account() { }")
										.WithConstructor("public Account(int amount) { }")
										.WithField("int _amount = 0;")
										.WithMethod("public void Add(int value) { this._amount += value; } ")
										.WithProperty("public int Amount { get { return this._amount; } set { this._amount = value; } }");

                string code = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] { interface1Builder, classBuilder });

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

					ITypeDeclaration introspectedInfo = result.Lookup(new TypeName(new FullyQualifiedName("Example.Account")));

					ClassDeclaration refDec = introspectedInfo as ClassDeclaration;

					Assert.NotNull(refDec);
					Assert.Equal(1, refDec.Interfaces.Count);
                    Assert.Contains(new TypeName(new FullyQualifiedName("Example.IEmptyInterface1")), refDec.Interfaces);
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsObjectImplementingSeveralInterfaces()
			{
				var interface1Builder = CompilableInterfaceBuilder.PublicInterface()
										.Named("IEmptyInterface1");

				var interface2Builder = CompilableInterfaceBuilder.PublicInterface()
										.Named("IEmptyInterface2");

				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account")
										.Implementing("IEmptyInterface1")
										.Implementing("IEmptyInterface2")
										.WithConstructor("public Account() { }")
										.WithConstructor("public Account(int amount) { }")
										.WithField("int _amount = 0;")
										.WithMethod("public void Add(int value) { this._amount += value; } ")
										.WithProperty("public int Amount { get { return this._amount; } set { this._amount = value; } }");

				string code = BoilerPlateCodeBuilder.BuildFor( new IBuildable[] { interface1Builder, interface2Builder, classBuilder });

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

					ITypeDeclaration introspectedInfo = result.Lookup(new TypeName(new FullyQualifiedName("Example.Account")));

					ClassDeclaration refDec = introspectedInfo as ClassDeclaration;

					Assert.NotNull(refDec);
					Assert.Equal(2, refDec.Interfaces.Count);
                    Assert.Contains(new TypeName(new FullyQualifiedName("Example.IEmptyInterface1")), refDec.Interfaces);
                    Assert.Contains(new TypeName(new FullyQualifiedName("Example.IEmptyInterface2")), refDec.Interfaces);
				}
			}

			[Fact, TouchingFileSystemTest]
			public void ReturnsObjectDerivedFromBase()
			{
				var baseClassBuilder = CompilableClassBuilder.PublicClass()
										.Named("BaseAccount");

				var classBuilder = CompilableClassBuilder.PublicClass()
										.Named("Account")
										.DerivedFrom("BaseAccount")
										.WithConstructor("public Account() { }")
										.WithConstructor("public Account(int amount) { }")
										.WithField("int _amount = 0;")
										.WithMethod("public void Add(int value) { this._amount += value; } ")
										.WithProperty("public int Amount { get { return this._amount; } set { this._amount = value; } }");

				string code = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] { baseClassBuilder, classBuilder });

				using (OnTheFlyAssembly otfAssembly = new OnTheFlyAssembly())
				{
					otfAssembly.Compile(code);

					var inspector = new MonoCecilAssemblyIntrospector();

					var result = inspector.IntrospectAssembly(otfAssembly.ProductAssembly);

                    ITypeDeclaration introspectedInfo = result.Lookup(new TypeName(new FullyQualifiedName("Example.Account")));

					ClassDeclaration refDec = introspectedInfo as ClassDeclaration;

					Assert.NotNull(refDec);
					Assert.Equal(0, refDec.Interfaces.Count);
					Assert.True(refDec.IsSubclass);
                    Assert.Equal(new TypeName(new FullyQualifiedName("Example.BaseAccount")), refDec.BaseType);
				}
			}
		}

		public class TheTypesInCommonWithMethod
		{
			[Fact, TouchingFileSystemTest]
			public void ReturnsListForSharedTypes()
			{
				string versionOneCode = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] {
                                        CompilableClassBuilder.PublicClass().Named("Account"),
                                        CompilableClassBuilder.PublicClass().Named("Branch") });

				string versionTwoCode = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] {
                                        CompilableClassBuilder.PublicClass().Named("Account"),
                                        CompilableClassBuilder.PublicClass().Named("Customer"),
                                        CompilableClassBuilder.PublicClass().Named("Branch") });

				using (OnTheFlyAssembly versionOneAssembly = new OnTheFlyAssembly())
				{
					versionOneAssembly.Compile(versionOneCode);

					using (OnTheFlyAssembly versionTwoAssembly = new OnTheFlyAssembly())
					{
						versionTwoAssembly.Compile(versionTwoCode);

						var inspector = new MonoCecilAssemblyIntrospector();

						var versionOneResults = inspector.IntrospectAssembly(versionOneAssembly.ProductAssembly);
						var versionTwoResults = inspector.IntrospectAssembly(versionTwoAssembly.ProductAssembly);

						var inCommon = versionTwoResults.TypesInCommonWith(versionOneResults);

                        Assert.Contains<TypeName>(new TypeName(new FullyQualifiedName("Example.Account")), inCommon);
                        Assert.Contains<TypeName>(new TypeName(new FullyQualifiedName("Example.Branch")), inCommon);
                        Assert.DoesNotContain<TypeName>(new TypeName(new FullyQualifiedName("Example.Customer")), inCommon);
					}
				}
			}
		}

		public class TheTypeDifferencesFromMethod
		{
			[Fact, TouchingFileSystemTest]
			public void ReturnsListForMismatchedTypes()
			{
				string versionOneCode = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] {
                                        CompilableClassBuilder.PublicClass().Named("Account"),
                                        CompilableClassBuilder.PublicClass().Named("Branch") });

				string versionTwoCode = BoilerPlateCodeBuilder.BuildFor(new IBuildable[] {
                                        CompilableClassBuilder.PublicClass().Named("Account"),
                                        CompilableClassBuilder.PublicClass().Named("Customer"),
                                        CompilableClassBuilder.PublicClass().Named("Branch") });

				using (OnTheFlyAssembly versionOneAssembly = new OnTheFlyAssembly())
				{
					versionOneAssembly.Compile(versionOneCode);

					using (OnTheFlyAssembly versionTwoAssembly = new OnTheFlyAssembly())
					{
						versionTwoAssembly.Compile(versionTwoCode);

						var inspector = new MonoCecilAssemblyIntrospector();

						var versionOneResults = inspector.IntrospectAssembly(versionOneAssembly.ProductAssembly);
						var versionTwoResults = inspector.IntrospectAssembly(versionTwoAssembly.ProductAssembly);

						var differences = versionTwoResults.TypeDifferencesFrom(versionOneResults);

                        Assert.Contains<TypeName>(new TypeName(new FullyQualifiedName("Example.Customer")), differences);
                        Assert.DoesNotContain<TypeName>(new TypeName(new FullyQualifiedName("Example.Account")), differences);
                        Assert.DoesNotContain<TypeName>(new TypeName(new FullyQualifiedName("Example.Branch")), differences);
					}
				}
			}
		}
	}
}
